/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import fr.x9c.cadmium.Infos;

/**
 * This class implements a mere data structure retaining all configuration
 * elements (parameters) that control the execution of an interpreter. <br/>
 * Instances of this class are not mutable (but its streams are, of course,
 * modifiable).
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.3
 * @since 1.0
 */
public final class ByteCodeParameters extends AbstractParameters {

    /** Prefix to interpreter arguments. */
    private static final String INTERP_ARG_PREFIX = "--";

    /** Whether gui dialog should be presented. */
    private final boolean showDialog;

    /** Whether help should be printed. */
    private final boolean showHelp;

    /** Whether version should be printed. */
    private final boolean showVersion;

    /** Whether disassembled program should be printed. */
    private final boolean showDisassembled;

    /** Initial stack size. */
    private final int initStackSize;

    /** Maximum stack size. */
    private final int maxStackSize;

    /** Additional primitive providers. */
    private final String[] providers;

    /** Whether dispatchers should be compiled. */
    private final boolean compileDispatchers;

    /**
     * Constructs interpreter parameters from explicit values.
     * @param arguments arguments to interpreted program
     *                  - should not be <tt>null</tt>
     * @param backtrace whether backtrace is requested
     * @param exitStoppingJVM whether <i>exit</i> primitive should stop JVM
     * @param in standard input of interpreted program
     *           - should not be <tt>null</tt>
     * @param out standard output of interpreted program
     *            - should not be <tt>null</tt>
     * @param err standard error of interpreted program
     *            - should not be <tt>null</tt>
     * @param awt whether <i>Graphics</i> frame should be AWT
     * @param bareCanvas whether <i>Graphics</i> component should be a bare
     *                   canvas (no frame)
     * @param javaxSound whether <tt>javax.sound</tt> should be used for
     *                   <i>Graphics</i> beeps
     * @param jdbm whether <i>JDBM</i> should be used as the <i>Dbm</i> implementation
     * @param os underlying OS identifier - should not be <tt>null</tt>
     * @param unixEmulation whether unix primitives should be emulated
     * @param embedded whether embedded mode is activated
     * @param embeddedBase base class for embedded mode
     * @param file file to be interpreted/executed
     * @param showDialog whether gui dialog should be presented
     *                   (only for internal use by main)
     * @param showHelp whether help should be printed
     *                   (only for internal use by main)
     * @param showVersion whether version should be printed
     *                   (only for internal use by main)
     * @param showDisassembled whether disassembled program should be printed
     *                   (only for internal use by main)
     * @param initStackSize initial stack size - should be > 512
     * @param maxStackSize maximum stack size
     *                     - should be >= <tt>initStackSize</tt>
     * @param providers additional primitive providers
     *                  - should not be <tt>null</tt>
     * @param compileDispatchers whether dispatchers should be compiled
     */
    public ByteCodeParameters(final String[] arguments,
                              final boolean backtrace,
                              final boolean exitStoppingJVM,
                              final InputStream in,
                              final PrintStream out,
                              final PrintStream err,
                              final boolean awt,
                              final boolean bareCanvas,
                              final boolean javaxSound,
                              final boolean jdbm,
                              final String os,
                              final boolean unixEmulation,
                              final String file,
                              final boolean embedded,
                              final String embeddedBase,
                              final boolean showDialog,
                              final boolean showHelp,
                              final boolean showVersion,
                              final boolean showDisassembled,
                              final int initStackSize,
                              final int maxStackSize,
                              final String[] providers,
                              final boolean compileDispatchers) {
        super(arguments, backtrace, exitStoppingJVM, in, out, err, awt,
              bareCanvas, javaxSound, jdbm, os, unixEmulation, file,
              embedded, embeddedBase);
        assert initStackSize > 512 : "initStackSize should be > 512";
        assert maxStackSize >= initStackSize
            : "maxStackSize should be >= initStackSize";
        assert providers != null : "null providers";
        this.showDialog = showDialog;
        this.showHelp = showHelp;
        this.showVersion = showVersion;
        this.showDisassembled = showDisassembled;
        this.initStackSize = initStackSize;
        this.maxStackSize = maxStackSize;
        this.providers = Arrays.copyOf(providers, providers.length);
        this.compileDispatchers = compileDispatchers;
    } // end constructor(String[], boolean, boolean, InputStream, ...)

    public ByteCodeParameters(String[] args, Object ... params) {
        this(args,
                ((Boolean)params[0]).booleanValue(), /* backtrace */
                ((Boolean)params[1]).booleanValue(), /* stop JVM */
                (InputStream)params[2], /* standard input */
                (PrintStream)params[3], /* standard output */
                (PrintStream)params[4], /* standard error */
                ((Boolean)params[5]).booleanValue(), /* awt */
                ((Boolean)params[6]).booleanValue(), /* bare canvas */
                ((Boolean)params[7]).booleanValue(), /* javax.sound beeps */
                ((Boolean)params[8]).booleanValue(), /* jdbm */
                (String)params[9], /* os */
                ((Boolean)params[10]).booleanValue(), /* unix emulation */
                (String)params[11], /* file */
                ((Boolean)params[12]).booleanValue(), /* embedded */
                (String)params[13], /* embedded base */
                ((Boolean)params[14]).booleanValue(), /* dialog */
                ((Boolean)params[15]).booleanValue(), /* help */
                ((Boolean)params[16]).booleanValue(), /* version */
                ((Boolean)params[17]).booleanValue(), /* disassemble */
                ((Integer)params[18]).intValue(), /* init stack size */
                ((Integer)params[19]).intValue(), /* max stack size */
                (String[])params[20], /* providers */
                ((Boolean)params[21]).booleanValue()); /* compile dispatcher */
    }

    /**
     * Constructs parameters for interpreter, all set to default values.
     * @return default parameters
     */
    public static ByteCodeParameters defaults() {
        return new ByteCodeParameters(new String[0], /* arguments */
                                      false,         /* backtrace */
                                      false,         /* stop JVM */
                                      System.in,     /* standard input */
                                      System.out,    /* standard output */
                                      System.err,    /* standard error */
                                      false,         /* awt */
                                      false,         /* bare canvas */
                                      false,         /* javax.sound beeps */
                                      false,         /* jdbm */
                                      "Unix",        /* os */
                                      false,         /* unix emulation */
                                      null,          /* file */
                                      false,         /* embedded */
                                      null,          /* embedded base */
                                      false,         /* dialog */
                                      false,         /* help */
                                      false,         /* version */
                                      false,         /* disassemble */
                                      64 * 1024,     /* init stack size */
                                      64 * 1024,     /* max stack size */
                                      new String[0], /* providers */
                                      true);         /* compile dispatcher */
    } // end method 'defaults()'

    /**
     * Constructs a map storing parameters.
     * @param that parameters - should not be <tt>null</tt>
     * @return map storing parameters
     */
    private static Map<String, Object> toMap(final ByteCodeParameters that) {
        assert that != null : "null that";
        final Map<String, Object> res = new HashMap<String, Object>();
        res.put("arguments", that.arguments);
        res.put("backtrace", that.backtrace);
        res.put("exitStoppingJVM", that.exitStoppingJVM);
        res.put("in", that.in);
        res.put("out", that.out);
        res.put("err", that.err);
        res.put("awt", that.awt);
        res.put("bareCanvas", that.bareCanvas);
        res.put("javaxSound", that.javaxSound);
        res.put("jdbm", that.jdbm);
        res.put("os", that.os);
        res.put("unixEmulation", that.unixEmulation);
        res.put("file", that.file);
        res.put("embedded", that.embedded);
        res.put("embeddedBase", that.embeddedBase);
        res.put("showDialog", that.showDialog);
        res.put("showHelp", that.showHelp);
        res.put("showVersion", that.showVersion);
        res.put("showDisassembled", that.showDisassembled);
        res.put("initStackSize", that.initStackSize);
        res.put("maxStackSize", that.maxStackSize);
        res.put("providers", that.providers);
        res.put("compileDispatchers", that.compileDispatchers);
        return res;
    } // end method 'toMap(ByteCodeParameters)'

    /**
     * Constructs an instance from parameters.
     * @param m map storing parameters - should not be <tt>null</tt>
     * @return parameters
     */
    private static ByteCodeParameters fromMap(final Map<String, Object> m) {
        assert m != null : "null m";
        return new ByteCodeParameters((String[])    m.get("arguments"),
                                      (Boolean)     m.get("backtrace"),
                                      (Boolean)     m.get("exitStoppingJVM"),
                                      (InputStream) m.get("in"),
                                      (PrintStream) m.get("out"),
                                      (PrintStream) m.get("err"),
                                      (Boolean)     m.get("awt"),
                                      (Boolean)     m.get("bareCanvas"),
                                      (Boolean)     m.get("javaxSound"),
                                      (Boolean)     m.get("jdbm"),
                                      (String)      m.get("os"),
                                      (Boolean)     m.get("unixEmulation"),
                                      (String)      m.get("file"),
                                      (Boolean)     m.get("embedded"),
                                      (String)      m.get("embeddedBase"),
                                      (Boolean)     m.get("showDialog"),
                                      (Boolean)     m.get("showHelp"),
                                      (Boolean)     m.get("showVersion"),
                                      (Boolean)     m.get("showDisassembled"),
                                      (Integer)     m.get("initStackSize"),
                                      (Integer)     m.get("maxStackSize"),
                                      (String[])    m.get("providers"),
                                      (Boolean)     m.get("compileDispatchers"));
    } // end method 'fromMap(Map<String, Object>)'

    /**
     * Constructs interpreter parameters from an array of string such as the
     * one received by the main method of an application.
     * @param args parameters as a string array - should not be <tt>null</tt>
     * @param dir current directory for command-line expansion <br/>
     *            If <tt>null</tt>, expansion is not performed.
     * @return corresponding interpreter parameters
     * @throws CadmiumException if a switch has an invalid format
     * @throws CadmiumException if an unknown switch is provided
     * @throws CadmiumException if a switch argument is invalid
     * @throws CadmiumException if maximum stack size is not >= init stack
     *                              size
     */
    public static ByteCodeParameters fromArray(final String[] args,
                                               final File dir)
        throws CadmiumException {
        assert args != null : "null args";
        final Map<String, Object> params = toMap(defaults());
        final int len = args.length;
        int idx = 0;
        while ((idx < len) && args[idx].startsWith(ByteCodeParameters.INTERP_ARG_PREFIX)) {
            final String[] parts = args[idx++].split("=");
            if (parts.length > 2) {
                throw new CadmiumException("invalid switch format");
            } // end if
            final String name = parts[0];
            final String value = parts.length > 1 ? parts[1] : null;
            if (name.equals("--gui")) {
                if (value != null) {
                    throw new CadmiumException("switch waits no argument");
                } // end if
                params.put("showDialog", Boolean.TRUE);
            } else if (name.equals("--help")) {
                if (value != null) {
                    throw new CadmiumException("switch waits no argument");
                } // end if
                params.put("showHelp", Boolean.TRUE);
            } else if (name.equals("--version")) {
                if (value != null) {
                    throw new CadmiumException("switch waits no argument");
                } // end if
                params.put("showVersion", Boolean.TRUE);
            } else if (name.equals("--disassemble")) {
                if (value != null) {
                    throw new CadmiumException("switch waits no argument");
                } // end if
                params.put("showDisassembled", Boolean.TRUE);
            } else if (name.equals("--init-stack-size")) {
                params.put("initStackSize", valueOfInteger(value));
            } else if (name.equals("--max-stack-size")) {
                params.put("maxStackSize", valueOfInteger(value));
            } else if (name.equals("--backtrace")) {
                params.put("backtrace", valueOfBoolean(value));
            } else if (name.equals("--stop-jvm")) {
                params.put("exitStoppingJVM", valueOfBoolean(value));
            } else if (name.equals("--providers")) {
                params.put("providers", valueOfProviders(value));
            } else if (name.equals("--compile-dispatchers")) {
                params.put("compileDispatcher", valueOfBoolean(value));
            } else if (name.equals("--use-awt")) {
                params.put("awt", valueOfBoolean(value));
            } else if (name.equals("--use-javax-sound")) {
                params.put("javaxSound", valueOfBoolean(value));
            } else if (name.equals("--use-jdbm")) {
                params.put("jdbm", valueOfBoolean(value));
            } else if (name.equals("--os")) {
                params.put("os", valueOfOS(value));
            } else if (name.equals("--unix-emulation")) {
                params.put("unixEmulation", valueOfBoolean(value));
            } else if (name.equals("--embedded")) {
                params.put("embedded", valueOfBoolean(value));
            } else if (name.equals("--embedded-base")) {
                params.put("embeddedBase", value);
            } else {
                throw new CadmiumException("unknown switch '" + name + "'");
            } // end if/elsif/else
        } // end while
        if (((Integer) params.get("initStackSize")) < 512) {
            throw new CadmiumException("initial stack size should be >= 512");
        } // end if
        if (((Integer) params.get("maxStackSize"))
            < ((Integer) params.get("initStackSize"))) {
            throw new CadmiumException("maximum stack size should be >= initial stack size");
        } // end if
        params.put("file", idx < len ? args[idx] : null);
        final int rem = len - idx;
        String[] appArgs = new String[rem];
        System.arraycopy(args, idx, appArgs, 0, rem);
        if (dir != null) {
            try {
                appArgs = Misc.expandCommandLine(dir, appArgs);
            } catch (final IOException ioe) {
                // just ignore command-line expansion
            } // end try/catch
        } // end if
        params.put("arguments", appArgs);
        return fromMap(params);
    } // end method 'fromArray(String[], File)'

    /**
     * Constructs interpreter parameters from properties and additional
     * elements. <br/>
     * The following properties and associated values are:
     * <ul>
     *   <li><tt>initStackSize</tt>: integer value;</li>
     *   <li><tt>maxStackSize</tt>: integer value;</li>
     *   <li><tt>backtrace</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>exitStoppingJVM</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>providers</tt> comma-separated list of class names;</li>
     *   <li><tt>compileDispatchers</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>awt</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>javaxSound</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>os</tt>: any of <tt>Unix</tt>, <tt>Cygwin</tt>,
     *       <tt>Win32</tt>, <tt>MacOS</tt>, or <tt>Cadmium</tt>;</li>
     *   <li><tt>unixEmulation</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>embedded</tt>: <tt>on</tt> or <tt>off</tt>;</li>
     *   <li><tt>embeddedBase</tt>: string value.</li>
     * </ul>
     * @param p properties - should not be <tt>null</tt>
     * @param file name of bytecode file to execute
     * @param args application arguments - should not be <tt>null</tt>
     * @param in standard input - should not be <tt>null</tt>
     * @param out standard output - should not be <tt>null</tt>
     * @param err standard error - should not be <tt>null</tt>
     * @return corresponding interpreter parameters
     */
    public static ByteCodeParameters fromProperties(final Properties p,
                                                    final String file,
                                                    final String[] args,
                                                    final InputStream in,
                                                    final PrintStream out,
                                                    final PrintStream err)
        throws CadmiumException {
        assert p != null : "null p";
        assert args != null : "null args";
        assert in != null : "null in";
        assert out != null : "null out";
        assert err != null : "null err";
        final Map<String, Object> params = toMap(defaults());
        params.put("file", file);
        params.put("arguments", args);
        params.put("initStackSize",
                   valueOfInteger(p.getProperty("initStackSize", "65536")));
        params.put("maxStackSize",
                   valueOfInteger(p.getProperty("maxStackSize", "65536")));
        params.put("backtrace",
                   valueOfBoolean(p.getProperty("backtrace", "off")));
        params.put("exitStoppingJVM",
                   valueOfBoolean(p.getProperty("exitStoppingJVM", "off")));
        params.put("providers", valueOfProviders(p.getProperty("providers")));
        params.put("in", in);
        params.put("out", out);
        params.put("err", err);
        params.put("compileDispatchers",
                   valueOfBoolean(p.getProperty("compileDispatchers", "on")));
        params.put("awt",
                   valueOfBoolean(p.getProperty("awt", "off")));
        params.put("javaxSound",
                   valueOfBoolean(p.getProperty("javaxSound", "off")));
        params.put("jdbm",
                   valueOfBoolean(p.getProperty("jdbm", "off")));
        params.put("os",
                   valueOfOS(p.getProperty("os", "Unix")));
        params.put("unixEmulation",
                   valueOfBoolean(p.getProperty("unixEmulation", "off")));
        params.put("embedded",
                   valueOfBoolean(p.getProperty("embedded", "off")));
        params.put("embeddedBase", p.getProperty("embeddedBase", ""));
        if (((Integer) params.get("initStackSize")) < 512) {
            throw new CadmiumException("initial stack size should be >= 512");
        } // end if
        if (((Integer) params.get("maxStackSize"))
            < ((Integer) params.get("initStackSize"))) {
            throw new CadmiumException("maximum stack size should be >= initial stack size");
        } // end if
        return fromMap(params);
    } // end method 'fromProperties(Properties)'

    /**
     * Stores interpreter parameters into a properties object. <br/>
     * Stored parameters are:
     * <ul>
     *   <li><tt>initStackSize</tt>;</li>
     *   <li><tt>maxStackSize</tt>;</li>
     *   <li><tt>backtrace</tt>;</li>
     *   <li><tt>exitStoppingJVM</tt>;</li>
     *   <li><tt>providers</tt>;</li>
     *   <li><tt>compileDispatchers</tt>;</li>
     *   <li><tt>awt</tt>;</li>
     *   <li><tt>javaxSound</tt>;</li>
     *   <li><tt>os</tt>;</li>
     *   <li><tt>unixEmulation</tt>;</li>
     *   <li><tt>embedded</tt>;</li>
     *   <li><tt>embeddedBase</tt>.</li>
     * </ul>
     * @param ip pararmeters to store - should not be <tt>null</tt>
     * @return properties object storing parameters
     */
    public static Properties toProperties(final ByteCodeParameters ip) {
        assert ip != null : "null ip";
        final Properties res = new Properties();
        res.setProperty("initStackSize", Integer.toString(ip.initStackSize));
        res.setProperty("maxStackSize", Integer.toString(ip.maxStackSize));
        res.setProperty("backtrace", Boolean.toString(ip.backtrace));
        res.setProperty("exitStoppingJVM", Boolean.toString(ip.exitStoppingJVM));
        res.setProperty("providers", providersToString(ip.providers));
        res.setProperty("compileDispatchers", Boolean.toString(ip.compileDispatchers));
        res.setProperty("awt", Boolean.toString(ip.awt));
        res.setProperty("javaxSound", Boolean.toString(ip.javaxSound));
        res.setProperty("jdbm", Boolean.toString(ip.jdbm));
        res.setProperty("os", ip.os);
        res.setProperty("unixEmulation", Boolean.toString(ip.unixEmulation));
        res.setProperty("embedded", Boolean.toString(ip.embedded));
        res.setProperty("embeddedBase", ip.embeddedBase);
        return res;
    } // end method 'toProperties(ByteCodeParameters)'

    /**
     * Prints the usage that is the format of switches that can be used on the
     * command line.
     * @param out where to print - should not be <tt>null</tt>
     */
    public static void printUsage(final PrintStream out) {
        assert out != null : "null out";
        out.println("Cadmium version " + Infos.VERSION + " (c) " + Infos.AUTHOR + ", " + Infos.YEARS);
        out.println("homepage: " + Infos.URL + " -- contact: " + Infos.MAIL);
        out.println("usage: <interpreter arguments> <filename> <application arguments>");
        out.println("possible interpreter arguments:");
        out.println("  --gui");
        out.println("      show GUI dialog to set parameters");
        out.println("  --help");
        out.println("      show this message");
        out.println("  --version");
        out.println("      show program version and exit");
        out.println("  --disassemble");
        out.println("      show disassembled program before execution");
        out.println("  --init-stack-size=<integer default:65536>");
        out.println("      set initial stack size, integer should be > 512");
        out.println("  --max-stack-size=<integer default:65536>");
        out.println("      set maximum stack size, integer should be >= initial stack size");
        out.println("  --backtrace=<on|off default:off>");
        out.println("      whether exception backtrace should be printed");
        out.println("  --stop-jvm=<on|off default:off>");
        out.println("      whether program end should stop JVM");
        out.println("  --providers=<comma-separated-list>");
        out.println("      list elements are fully qualified class names to be used as providers");
        out.println("  --compile-dispatchers=<on|off default:on>");
        out.println("      whether primitive dispatchers should be compiled");
        out.println("  --use-awt=<on|off default:off>");
        out.println("      whether Graphics library should use AWT");
        out.println("  --use-javax-sound=<on|off default:off>");
        out.println("      whether Graphics library should use javax.sound");
        out.println("  --use-jdbm=<on|off default:off>");
        out.println("      whether Dbm library should use JDBM");
        out.println("  --os=<Unix|Cygwin|Win32|MacOS|Cadmium default:Unix>");
        out.println("      how underlying OS should be identified");
        out.println("  --unix-emulation=<on|off default:off>");
        out.println("      whether unix primitives should be emulated by program execution");
        out.println("  --embedded=<on|off default:off>");
        out.println("      whether embedded mode should be activated");
        out.println("  --embedded-base=<fully-qualified class name>");
        out.println("      base class used by embedded mode");
    } // end method 'printUsage(PrintStream)'

    /**
     * Tests whether gui dialog should be presented for parameters choice.
     * @return <tt>true</tt> if gui dialog should be presented,
     *         <tt>false</tt> otherwise
     */
    public boolean showDialog() {
        return this.showDialog;
    } // end method 'showDialog()'

    /**
     * Tests whether help should be printed.
     * @return <tt>true</tt> if help should be printed,
     *         <tt>false</tt> otherwise
     */
    public boolean showHelp() {
        return this.showHelp;
    } // end method 'showHelp()'

    /**
     * Tests whether version should be printed.
     * @return <tt>true</tt> if version should be printed,
     *         <tt>false</tt> otherwise
     */
    public boolean showVersion() {
        return this.showVersion;
    } // end method 'showVersion()'

    /**
     * Tests whether disassembled version should be printed.
     * @return <tt>true</tt> if disassembled version should be printed,
     *         <tt>false</tt> otherwise
     */
    public boolean showDisassembled() {
        return this.showDisassembled;
    } // end method 'showDisassembled()'

    /**
     * Returns initial stack size (for main thread).
     * @return initial stack size
     */
    public int getInitStackSize() {
        return this.initStackSize;
    } // end method 'getInitStackSize()'

    /**
     * Returns maximum stack size.
     * @return maximum stack size
     */
    public int getMaxStackSize() {
        return this.maxStackSize;
    } // end method 'getMaxStackSize()'

    /**
     * Returns the additional primitive providers to be used.
     * @return the additional primitive providers to be used
     */
    public String[] getProviders() {
        return Arrays.copyOf(this.providers, this.providers.length);
    } // end method 'getProviders()'

    /**
     * Tests whether dispatcher should be compiled.
     * @return <tt>true</tt> if dispatcher should be compiled,
     *         <tt>false</tt> otherwise
     */
    public boolean isDispatcherCompiled() {
        return this.compileDispatchers;
    } // end method 'isDispatcherCompiled()'

} // end class 'ByteCodeParameters'
